/**
 *  Copyright 2013 University Pierre & Marie Curie - UMR CNRS 7606 (LIP6/MoVe)
 *  All rights reserved.   This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  Initial contributor:
 *    Lom M. Hillah - <lom-messan.hillah@lip6.fr>
 *
 *  Mailing list:
 *    lom-messan.hillah@lip6.fr
 */
package fr.lip6.msr4j.asf.datamodel;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.slf4j.Logger;

import fr.lip6.msr4j.utils.config.MSR4JLogger;

/**
 * Maps ASF SVN projects to their committers, SVN Projects names to their
 * respective object representation.
 * 
 * @see ASFCommitter
 * @see SVNProject
 * @author lom
 * 
 */
public final class ASFProjectsCommitters {
	/**
	 * Name of this object (basically ASF Projects)
	 */
	private final String name;
	/**
	 * Represents an unknown project, for committers to whom no project is
	 * assigned.
	 */
	private final SVNProject voidPr;
	/**
	 * All known committers.
	 */
	private final Set<ASFCommitter> committers;
	/**
	 * Committers associated to their names as keys.
	 */
	private final Map<String, ASFCommitter> nameCommitters;
	private final Map<String, SVNProject> svnprojects;
	private final Map<SVNProject, Set<ASFCommitter>> projectCommitters;
	private final Logger logger;
	private int newlySVNProjAdded;
	private int newlyCommAdded;
	private Map<String, ASFTopLevelProject> tlps;
	private Map<String, ASFProject> projects;

	public ASFProjectsCommitters(String name) {
		this.name = name;
		svnprojects = new HashMap<String, SVNProject>();
		projectCommitters = new HashMap<SVNProject, Set<ASFCommitter>>();
		committers = new HashSet<ASFCommitter>();
		nameCommitters = new HashMap<String, ASFCommitter>();
		tlps = new HashMap<String, ASFTopLevelProject>();
		projects = new HashMap<String, ASFProject>();
		// Special case for "committers" not involved in any project
		voidPr = new SVNProject("NO-PROJECT");
		projectCommitters.put(voidPr, voidPr.getCommitters());
		svnprojects.put(voidPr.getName(), voidPr);
		logger = MSR4JLogger.getLogger(this.getClass().getCanonicalName());
	}

	public Set<SVNProject> getSVNProjects() {
		return projectCommitters.keySet();
	}

	public Set<ASFCommitter> getCommitters() {
		return committers;
	}

	public boolean addSVNProject(SVNProject p) {
		boolean result = false;
		if (!this.projectCommitters.containsKey(p)) {
			this.projectCommitters.put(p, p.getCommitters());
			this.committers.addAll(p.getCommitters());
			registerCommittersNames(p.getCommitters());
			this.svnprojects.put(p.getName(), p);
			result = true;
		}
		return result;
	}

	/**
	 * Registers the committer and the SVN projects he is involved in.
	 * 
	 * @param cpr
	 *            the SVN projects the committer is involved in
	 * @param c
	 *            the committer
	 */
	public void addProjects(String[] cpr, ASFCommitter c) {
		this.committers.add(c);
		this.nameCommitters.put(c.getSvnId(), c);
		if (cpr != null) {
			for (String s : cpr) {
				if (!"".equals(s)) {
					SVNProject p = new SVNProject(s);
					if (this.projectCommitters.containsKey(p)) {
						SVNProject actual = getSVNProjectByName(p);
						c.addProject(actual);
					} else {
						c.addProject(p);
						this.projectCommitters.put(p, p.getCommitters());
						this.svnprojects.put(p.getName(), p);
					}
				} else {
					// the committer is not involved in any project
					c.addProject(voidPr);
					logger.warn(
							"Reporting committer not involved in any SVN project: {}.",
							c.getName());
				}
			}
		}
	}

	public SVNProject getSVNProjectByName(SVNProject p) {
		Set<String> keys = this.svnprojects.keySet();
		SVNProject result = null;
		for (String s : keys) {
			if (normalizeCheckEqual(s, p.getName().replace("(incubating)", ""))) {
				result = this.svnprojects.get(s);
				break;
			}
		}
		return result;
	}

	public SVNProject getSVNProjectByName(String name) {
		Set<String> keys = this.svnprojects.keySet();
		SVNProject result = null;
		for (String s : keys) {
			if (normalizeCheckEqual(s, name.replace("(incubating)", ""))) {
				result = this.svnprojects.get(s);
				break;
			}
		}
		return result;
	}

	public String getName() {
		return name;
	}

	@Override
	public String toString() {
		return "Projects [name=" + name + ", projectCommitters="
				+ projectCommitters + "]";
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		ASFProjectsCommitters other = (ASFProjectsCommitters) obj;
		if (name == null) {
			if (other.name != null) {
				return false;
			}
		} else if (!name.equals(other.name)) {
			return false;
		}
		return true;
	}

	/**
	 * Processes the committers associated to an SVN project, by populating
	 * internal maps, if some data was missing (e.g. a committer of project
	 * previously unknown).
	 * 
	 * @param projComm
	 *            Associations between an SVN project's name and its committers
	 *            (svn id, name).
	 * @return the number of new insertions (successfully retained projects and
	 *         committers). The number of newly respectively retained committers
	 *         and projects will be accessible through
	 *         {@link ASFProjectsCommitters#getNewlyCommAdded()} and
	 *         {@link ASFProjectsCommitters#getNewlySVNProjAdded()} after this
	 *         method has completed.
	 */
	public int addCommittersBySVNProject(
			final Map<String, Map<String, String>> projComm) {
		final Set<Entry<String, Map<String, String>>> es = projComm.entrySet();
		String prjName = null;
		Set<Entry<String, String>> commitrs = null;
		ASFCommitter asfComm = null;
		SVNProject pr = null;
		for (Entry<String, Map<String, String>> en : es) {
			prjName = en.getKey();
			pr = getSVNProjectByName(prjName);
			if (pr == null) {
				pr = new SVNProject(prjName);
				newlySVNProjAdded++;
				logger.warn("Reporting a newly added SVN Project: {}", prjName);
			}
			commitrs = en.getValue().entrySet();
			for (Entry<String, String> comm : commitrs) {
				asfComm = getCommitterById(comm.getKey());
				if (asfComm == null) {
					// We create the Committer, without any info about
					// whether s/he is ASF member of emeritus member.
					asfComm = new ASFCommitter(comm.getKey(), comm.getValue());
					newlyCommAdded++;
					logger.warn("Reporting a newly added Committer : {}",
							asfComm.getName());
				}
				if (!asfComm.hasProject(pr)) {
					asfComm.addProject(pr);
				}
			}
			addSVNProject(pr);
		}
		return newlySVNProjAdded + newlyCommAdded;
	}

	public ASFCommitter getCommitterById(String id) {
		return this.nameCommitters.get(id);
	}

	private void registerCommittersNames(Set<ASFCommitter> comm) {
		for (ASFCommitter c : comm) {
			this.nameCommitters.put(c.getSvnId(), c);
		}
	}

	/**
	 * Returns the number of new SVN Projects inserted after the call to
	 * {@link ASFProjectsCommitters#addCommittersByProject}.
	 * 
	 * @return the number of new SVN Projects inserted
	 */
	public int getNewlySVNProjAdded() {
		return newlySVNProjAdded;
	}

	/**
	 * Returns the number of new Committers inserted after the call to
	 * {@link ASFProjectsCommitters#addCommittersByProject}.
	 * 
	 * @return the number of new Committers inserted
	 */
	public int getNewlyCommAdded() {
		return newlyCommAdded;
	}

	public void setNewlySVNProjAdded(int newlySVNProjAdded) {
		this.newlySVNProjAdded = newlySVNProjAdded;
	}

	public void setNewlyCommAdded(int newlyCommAdded) {
		this.newlyCommAdded = newlyCommAdded;
	}

	/**
	 * Add committers from the map parameter to the internal collections of this
	 * class. The map contains : <svnId, [name, homepage]. The primary intent of
	 * this method is to retrieve homepages and complete the committers'
	 * profiles hold in this class with that new information.
	 * 
	 * @param map
	 *            the map containing <svnId, [name, homepage]> for each
	 *            committer.
	 */
	public void addCommittersHomePageById(final Map<String, String[]> map) {
		ASFCommitter c;
		for (Entry<String, String[]> e : map.entrySet()) {
			c = checkCommitterById(e.getKey(), map);
			c.setWebSite(e.getValue()[1]);
		}
	}

	/**
	 * Sets ASF Members.
	 * 
	 * @param map
	 *            <svnId, [name, homepage]>
	 */
	public void addASFMemberById(Map<String, String[]> map) {
		ASFCommitter c;
		for (Entry<String, String[]> e : map.entrySet()) {
			c = checkCommitterById(e.getKey(), map);
			c.setApacheMember(true);
			if (e.getValue()[1] != null) {
				c.setWebSite(e.getValue()[1]);
			}
		}
	}

	/**
	 * Sets ASF Emeritus Members.
	 * 
	 * @param map
	 *            <svnId, [name, homepage]>
	 */
	public void addASFEmeritusMemberById(Map<String, String[]> map) {
		ASFCommitter c;
		for (Entry<String, String[]> e : map.entrySet()) {
			c = checkCommitterById(e.getKey(), map);
			c.setEmeritusMember(true);
			if (e.getValue()[1] != null) {
				c.setWebSite(e.getValue()[1]);
			}
		}
	}

	private ASFCommitter checkCommitterById(String id,
			final Map<String, String[]> map) {
		ASFCommitter c = getCommitterById(id);
		if (c == null) {
			c = new ASFCommitter(id, map.get(id)[0]);
			setNewlyCommAdded(getNewlyCommAdded() + 1);
			this.committers.add(c);
			this.nameCommitters.put(c.getName(), c);
			// For now the projects of this discovered committer/member are
			// unknown.
			c.addProject(voidPr);
			logger.warn("Reporting a newly added Committer : {} --> {}", id,
					c.getName());
		}
		return c;
	}

	public void addASFTopLevelProject(ASFTopLevelProject asfTopLevelProject) {
		this.tlps.put(asfTopLevelProject.getName(), asfTopLevelProject);
	}

	public Collection<ASFTopLevelProject> getTopLevelProjects() {
		return this.tlps.values();
	}

	/**
	 * Add an ASF Project to the interal collection of ASF Projects.
	 * 
	 * @param asfProject
	 */
	public void addASFProject(ASFProject asfProject) {
		this.projects.put(asfProject.getName(), asfProject);
	}

	public Collection<ASFProject> getASFProjects() {
		return this.projects.values();
	}

	/**
	 * Returns true if the project was added (meaning it is a new one), false
	 * otherwise (meaning it already exists in the internal collection).
	 * 
	 * @param asfProject
	 *            the project to add into the internal collection
	 * @return true if it was successfully added (meaning it is a new one)
	 */
	public boolean checkAddASFProject(ASFProject asfProject) {
		boolean result = false;
		if (!this.projects.containsKey(asfProject.getName())) {
			this.projects.put(asfProject.getName(), asfProject);
			result = true;
			logger.warn("Reporting a newly added ASF project: {}.", asfProject);
		}
		return result;
	}

	/**
	 * Associate ASF Projects to their respective TLP (in this case TLP ~ PMC)
	 * 
	 * @param map
	 *            contain a TLP name and the set of its managedprojects.
	 */
	public void checkRelatePMCToASFProjects(Map<String, List<ASFProject>> map) {
		String prName = map.keySet().iterator().next();
		ASFTopLevelProject tlp = getTLPByName(prName);
		if (tlp == null) {
			tlp = new ASFTopLevelProject(prName);
			addASFTopLevelProject(tlp);
			logger.warn("Reporting a newly added ASF TLP project: {}.",
					tlp.getName());
		}
		List<ASFProject> prs = map.get(prName);
		for (ASFProject p : prs) {
			if (checkAddASFProject(p)) {
				p.setTopLevelProject(tlp);
			} else {
				ASFProject actual = getASFProjectByName(p.getName());
				actual.setTopLevelProject(tlp);
			}
		}
	}

	public ASFProject getASFProjectByName(String n) {
		return this.projects.get(n);
	}

	/**
	 * Returns a Top Level Project, given its name.
	 * 
	 * @param name
	 *            the name of the TLP
	 * @return the TLP, null if it does not exist.
	 */
	public ASFTopLevelProject getTLPByName(String name) {
		Set<String> keys = this.tlps.keySet();
		ASFTopLevelProject result = null;
		for (String s : keys) {
			if (normalizeCheckContain(s, name, ContainmentCheckDirection.BOTH)) {
				result = this.tlps.get(s);
				break;
			}
		}
		return result;
	}

	/**
	 * "Normalizes" two strings and check whether either is contained in the
	 * other. Normalization: trim, remove internal spaces and turn into lower
	 * case.
	 * 
	 * @param s
	 * @param t
	 * @param direction
	 *            in which direction to make the check. Both is check with the
	 *            boolean connective 'or', not 'and' (otherwise you would be
	 *            explicitly checking for equality, which is not the purpose of
	 *            this method).
	 * @return whether either (of t or s) is contained in the other.
	 * @see {@link #normalizeCheckEqual}
	 */
	private boolean normalizeCheckContain(String s, String t,
			ContainmentCheckDirection direction) {
		String normS = s.trim().replaceAll("\\s", "").toLowerCase();
		String normT = t.trim().replaceAll("\\s", "").toLowerCase();
		boolean result = false;
		switch (direction) {
		case LEFT_RIGHT:
			result = normS.contains(normT);
			break;
		case RIGHT_LEFT:
			result = normT.contains(normS);
			break;
		case BOTH:
			result = normS.contains(normT) || normT.contains(normS);
			break;
		default:
			break;
		}
		return result;
	}

	private boolean normalizeCheckEqual(String s, String t) {
		String normS = s.trim().replaceAll("\\s", "").toLowerCase();
		String normT = t.trim().replaceAll("\\s", "").toLowerCase();
		return normS.equals(normT);
	}

}
